/*************************************************************************** 
 * (c) 2006 Dmitry Osinovsky, osdmsoftware@gmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU Lesser General Public License as        *
 *   published by the Free Software Foundation; either version 2 of the    *
 *   License, or (at your option) any later version.                       *
 *                                                                         *
 ***************************************************************************/

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection;
using System.Collections;
using System.Drawing;
using System.Diagnostics;
using System.Globalization;
using System.Windows.Forms;

namespace OsdmSoftware.CommonLibrary
{
    public class UniqueNameAttribute: Attribute
    {
        public UniqueNameAttribute() { }
    }

    public class SerializeThisPropertyAttribute: Attribute
    {
        public SerializeThisPropertyAttribute() { }
    }

    public class SerializeAsNestedAttribute: Attribute
    {
        public SerializeAsNestedAttribute() { }
    }

    public class SerializeAsObjectAttribute: Attribute
    {
        public SerializeAsObjectAttribute() { }
    }

    public class DoNotSerializeAttribute: Attribute
    {
        public DoNotSerializeAttribute() { }
    }

    public class ChangedTypeAttribute: Attribute
    {
        string oldType;
        int fromVersion;
        int toVersion;

        public string OldType { get { return oldType; } }
        public int FromVersion { get { return fromVersion; } }
        public int ToVersion { get { return toVersion; } }

        public ChangedTypeAttribute(string oldType, int fromVersion, int toVersion)
        {
            this.oldType = oldType;
            this.fromVersion = fromVersion;
            this.toVersion = toVersion;
        }
    }

    public interface ISerializeControl
    {
        bool BeforeSave(Serialization sr, BinaryWriter stream);
        void AfterSave(Serialization sr, BinaryWriter stream);

        bool BeforeLoad(Serialization sr, BinaryReader stream);
        void AfterLoad(Serialization sr, BinaryReader stream);
    }

    public interface IFinalizeAndValidate
    {
        bool FinalizeAndValidate();
    }

    public class SerializeFormatException: ApplicationException
    {
        public SerializeFormatException(string message) : base(message) { }
    }

    public class Serialization
    {
        Dictionary<string, object> loadedObjects = new Dictionary<string, object>();
        Dictionary<string, object> linkedObjects = new Dictionary<string, object>();
        public Dictionary<string, object> LoadedObjects { get { return loadedObjects; } }
        bool serializeOnlyMarkedAsSerializable = false;
        int version;

        public bool SerializeOnlyMarkedAsSerializable { get { return serializeOnlyMarkedAsSerializable; } set { serializeOnlyMarkedAsSerializable = value; } }
        public int Version { get { return version; } set { version = value; } }

        public static object MakeCopy(object obj)
        {
            return MakeCopy(obj, false);
        }

        public static object MakeCopy(object obj, bool serializeOnlyMarkedAsSerializable)
        {
            Serialization sr = new Serialization();
            sr.SerializeOnlyMarkedAsSerializable = serializeOnlyMarkedAsSerializable;
            Stream s = new MemoryStream();
            BinaryWriter bw = new BinaryWriter(s);
            sr.SaveObject(obj, bw);
            s.Seek(0, SeekOrigin.Begin);
            BinaryReader br = new BinaryReader(s);
            object res = sr.LoadObject(br);
            s.Close();
            return res;
        }

        public void SaveObjLink(object o, BinaryWriter stream)
        {
            if (o == null)
                stream.Write("null");
            else
            {
                string s1 = o.GetType().FullName;
                stream.Write(s1);
                string s2 = (string) o.GetType().GetProperty("Name").GetValue(o, null);
                stream.Write(s2);
                string s3 = s1 + "&&" + s2;
                linkedObjects[s3] = o;
            }
        }

        public void SaveObjProperties(object obj, BinaryWriter stream)
        {
            ISerializeControl sc = obj as ISerializeControl;
            bool process = true;
            if (sc != null)
                process = sc.BeforeSave(this, stream);

            if (!process)
                return;

            Type t = obj.GetType();
            PropertyInfo[] props = t.GetProperties();
            foreach (PropertyInfo p in props)
            {
                Type pt = p.PropertyType;
                DoNotSerializeAttribute dnts = (DoNotSerializeAttribute) Attribute.GetCustomAttribute(p, typeof(DoNotSerializeAttribute));
                if (dnts != null)
                    continue;

                if (serializeOnlyMarkedAsSerializable)
                {
                    SerializeThisPropertyAttribute stpa = (SerializeThisPropertyAttribute) Attribute.GetCustomAttribute(p, typeof(SerializeThisPropertyAttribute));
                    if (stpa == null)
                        continue;
                }

                SerializeAsNestedAttribute sa = (SerializeAsNestedAttribute) Attribute.GetCustomAttribute(p, typeof(SerializeAsNestedAttribute));
                if (sa != null)
                {
                    stream.Write(p.Name);
                    Object o = p.GetValue(obj, null);
                    if (o is IList)
                    {
                        IList l = (IList) o;
                        if (l.IsFixedSize)
                        {
                        }
                        else
                        {
                            SaveDynamicCollection(l, stream);
                        }
                    }
                    else if (o is IDictionary)
                    {
                        IDictionary d = (IDictionary) o;
                        SaveDictionary(d, stream);
                    }
                    else
                    {
                        SaveObjProperties(o, stream);
                    }
                }
                else
                {
                    if (!(p.CanWrite && p.CanRead))
                    {
                        if (serializeOnlyMarkedAsSerializable)
                        {
                            Trace.Assert(false, "Property " + p.Name + " in class " + t.FullName + " is marked as serializable, but has no \"get\" or \"set\" clause. Maybe it should be marked with SerializeAsNestedAttribute?");
                        }
                        continue;
                    }

                    stream.Write(p.Name);

                    SaveValue(p.GetValue(obj, null), pt, true, Attribute.GetCustomAttribute(p, typeof(SerializeAsObjectAttribute)) != null, stream, "property " + p.Name + " in class " + t.FullName);
                }
            }

            stream.Write("End of properties");
            if (sc != null)
                sc.AfterSave(this, stream);
        }

        public object LoadObjLink(BinaryReader stream)
        {
            object o = null;
            string s1 = stream.ReadString();
            if (s1 != "null")
            {
                string s2 = stream.ReadString();
                string s3 = s1 + "&&" + s2;
                if (!loadedObjects.TryGetValue(s3, out o))
                {
                    if (!linkedObjects.TryGetValue(s3, out o))
                    {
                        throw new SerializeFormatException("Object " + s3 + " not found in serialized object collection");
                    }
                }
            }
            return o;
        }

        public void LoadObjProperties(object obj, BinaryReader stream)
        {
            ISerializeControl sc = obj as ISerializeControl;
            bool process = true;
            if (sc != null)
                process = sc.BeforeLoad(this, stream);

            if (!process)
                return;

            Type t = obj.GetType();
            string s = stream.ReadString();
            while (s != "End of properties")
            {
                PropertyInfo p = t.GetProperty(s);
                if (p == null)
                {
                    throw new SerializeFormatException("Loaded serialized property " + s + " for object " + obj.GetType().FullName + " that is not contained in that type");
                }

                Type pt = p.PropertyType;
                //UniqueNameAttribute una = (UniqueNameAttribute) Attribute.GetCustomAttribute(pt, typeof(UniqueNameAttribute), true);
                SerializeAsNestedAttribute sa = (SerializeAsNestedAttribute) Attribute.GetCustomAttribute(p, typeof(SerializeAsNestedAttribute));
                if (sa != null)
                {
                    Object o = p.GetValue(obj, null);
                    if (o is IList)
                    {
                        IList l = (IList) o;
                        if (l.IsFixedSize)
                        {
                        }
                        else
                        {
                            LoadDynamicCollection(l, stream);
                        }
                    }
                    else if (o is IDictionary)
                    {
                        IDictionary d = (IDictionary) o;
                        LoadDictionary(d, stream);
                    }
                    else
                    {
                        LoadObjProperties(o, stream);
                    }
                }
                else
                {
                    Type newPt = pt;

                    Attribute[] ctas = Attribute.GetCustomAttributes(p, typeof(ChangedTypeAttribute));
                    foreach (ChangedTypeAttribute cta in ctas)
                    {
                        if (cta.FromVersion <= version && version <= cta.ToVersion)
                        {
                            pt = Type.GetType(cta.OldType);
                            break;
                        }
                    }

                    object value = LoadValue(pt, true, Attribute.GetCustomAttribute(p, typeof(SerializeAsObjectAttribute)) != null, stream, "property " + p.Name + " for object " + obj.GetType().FullName);
                    if (newPt == pt)
                        p.SetValue(obj, value, null);
                    else
                        p.SetValue(obj, Convert.ChangeType(value, newPt, CultureInfo.InvariantCulture.NumberFormat), null);
                }
                s = stream.ReadString();
            }

            if (sc != null)
                sc.AfterLoad(this, stream);
        }

        public void SaveObject(object obj, BinaryWriter stream)
        {
            SaveObject(obj, stream, true);
        }

        public void SaveObject(object obj, BinaryWriter stream, bool writeType)
        {
            if (obj == null)
            {
                if (writeType)
                    stream.Write("null");
            }
            else
            {
                if (writeType)
                    stream.Write(obj.GetType().FullName);
                SaveObjProperties(obj, stream);
            }
        }

        public object LoadObject(BinaryReader stream)
        {
            string s = stream.ReadString();
            return LoadObject(stream, s);
        }

        public object LoadObject(BinaryReader stream, string typeName)
        {
            string s = typeName;
            if (s == "null")
                return null;
            Type t = Type.GetType(s);
            if (t == null)
                t = Type.GetType("OsdmSoftware." + s);
            ConstructorInfo c = t.GetConstructor(new Type[0]);
            object o = c.Invoke(null);
            LoadObjProperties(o, stream);
            Object[] attrs = t.GetCustomAttributes(typeof(UniqueNameAttribute), true);
            if (attrs.Length > 0)
            {
                string s1 = /*t.FullName*/ s + "&&" + (string) t.GetProperty("Name").GetValue(o, null);
                if (loadedObjects.ContainsKey(s1))
                {
                    MessageBox.Show("Loaded objects already contain " + s1);
                    return loadedObjects[s1];
                }
                loadedObjects.Add(s1, o);
            }
            return o;
        }

        public void SaveValue(object value, bool asObject, BinaryWriter stream, string errorContext)
        {
            if (value == null)
            {
                stream.Write("null");
            }
            else
            {
                stream.Write(value.GetType().FullName);
                SaveValue(value, value.GetType(), false, asObject, stream, errorContext);
            }
        }

        public void SaveValue(object value, Type type, bool writeType, bool asObject, BinaryWriter stream, string errorContext)
        {
            if ((type.IsClass && !type.Equals(typeof(string))) || type.IsInterface)
            {
                if (asObject)
                {
                    SaveObject(value, stream, writeType);
                }
                else
                {
                    UniqueNameAttribute una = (UniqueNameAttribute) Attribute.GetCustomAttribute(type, typeof(UniqueNameAttribute), true);
                    if (una != null)
                    {
                        SaveObjLink(value, stream);
                    }
                    else
                    {
                        Trace.Assert(false, "Serializing " + errorContext + ", type " + type.FullName + " is class or interface and cannot be serialized. Maybe property should be marked with SerializeAsObjectAttribute or type should have UniqueNameAttribute?");
                    }
                }
            }
            else
            {
                if (type.Equals(typeof(int)))
                {
                    stream.Write((int) value);
                }
                else if (type.Equals(typeof(double)))
                {
                    stream.Write((double) value);
                }
                else if (type.Equals(typeof(bool)))
                {
                    stream.Write((bool) value);
                }
                else if (type.Equals(typeof(Color)))
                {
                    Color c = (Color) value;
                    stream.Write(c.ToArgb());
                }
                else if (type.Equals(typeof(string)))
                {
                    object o = value;
                    if (o == null)
                        stream.Write("");
                    else
                        stream.Write((string) o);
                }
                else if (type.IsEnum)
                {
                    stream.Write(value.ToString());
                }
                else
                {
                    Trace.Assert(false, "Serializing " + errorContext + ", type " + type.FullName + " is not supported");
                }
            }
        }

        public object LoadValue(bool asObject, BinaryReader stream, string errorContext)
        {
            string s = stream.ReadString();
            if (s == "null")
                return null;
            Type t = Type.GetType(s);
            if (t == null)
                t = Type.GetType("OsdmSoftware." + s);

            return LoadValue(t, false, asObject, stream, errorContext);
        }

        public object LoadValue(Type type, bool loadType, bool asObject, BinaryReader stream, string errorContext)
        {
            if ((type.IsClass && !type.Equals(typeof(string))) || type.IsInterface)
            {
                if (asObject)
                {
                    if (loadType)
                        return LoadObject(stream);
                    else
                        return LoadObject(stream, type.FullName);
                }
                else
                {
                    UniqueNameAttribute una = (UniqueNameAttribute) Attribute.GetCustomAttribute(type, typeof(UniqueNameAttribute), true);
                    if (una != null)
                    {
                        return LoadObjLink(stream);
                    }
                    else
                    {
                        Trace.Assert(false, "Serializing " + errorContext + ", type " + type.FullName + " is class or interface and cannot be serialized. Maybe property should be marked with SerializeAsObjectAttribute or type should have UniqueNameAttribute?");
                        return null;
                    }
                }
            }
            else
            {
                object value;

                if (type.Equals(typeof(int)))
                {
                    value = stream.ReadInt32();
                }
                else if (type.Equals(typeof(double)))
                {
                    value = stream.ReadDouble();
                }
                else if (type.Equals(typeof(bool)))
                {
                    value = stream.ReadBoolean();
                }
                else if (type.Equals(typeof(Color)))
                {
                    int i = stream.ReadInt32();
                    value = Color.FromArgb(i);
                }
                else if (type.Equals(typeof(string)))
                {
                    value = stream.ReadString();
                }
                else if (type.IsEnum)
                {
                    string s1 = stream.ReadString();
                    value = Enum.Parse(type, s1, false);
                }
                else
                {
                    Trace.Assert(false, "Serializing " + errorContext + ", type " + type.FullName + " is not supported");
                    value = null;
                }

                return value;
            }
        }

        public void SaveDynamicCollection(IList list, BinaryWriter stream)
        {
            foreach (object obj in list)
            {
                stream.Write("New object");
                SaveValue(obj, true, stream, "collection");
                //SaveObject(obj, stream);
            }

            stream.Write("End of collection");
        }

        public void LoadDynamicCollection(IList list, BinaryReader stream)
        {
            list.Clear();
            string s = stream.ReadString();
            while (s != "End of collection")
            {
                if (s != "New object")
                {
                    throw new SerializeFormatException("\"New object\" expected but " + s + " found");
                }

                Object o = LoadValue(true, stream, "collection");
                //Object o = LoadObject(stream);
                if (!list.Contains(o))
                    list.Add(o);
                s = stream.ReadString();
            }
        }

        public void SaveDictionary(IDictionary list, BinaryWriter stream)
        {
            foreach (DictionaryEntry pair in list)
            {
                stream.Write("New pair");
                SaveValue(pair.Key, true, stream, "dictionary key");
                SaveValue(pair.Value, true, stream, "dictionary value");
                //SaveValue(obj, true, stream, "collection");
                //SaveObject(obj, stream);
            }

            stream.Write("End of collection");
        }

        public void LoadDictionary(IDictionary list, BinaryReader stream)
        {
            list.Clear();
            string s = stream.ReadString();
            while (s != "End of collection")
            {
                if (s != "New pair")
                {
                    throw new SerializeFormatException("\"New pair\" expected but " + s + " found");
                }

                Object key = LoadValue(true, stream, "dictionary key");
                Object value = LoadValue(true, stream, "dictionary value");

                if (!list.Contains(key))
                    list.Add(key, value);
                s = stream.ReadString();
            }
        }

    }
}
